2024.6.19 プログラムの高速化に関する知見(tqdm, 関数)
~~~~~~~~~~~~~~~~~~
実行環境:
Core i9-12900 2.4GHz, 16GB
Windows11 Home 23H2 64bit
WSL2 + Python3
~~~~~~~~~~~~~~~~~~
経過時間の計測にはtimeモジュールを利用する。まず、CPU timeを計測するprocess_timeの比較を行う。
code:jikan1.py
import time
loopmax = 1000000000
count = 0
time_t1 = time.time()
time_p1 = time.process_time()
#
for i in range(loopmax):
count += 1
#
time_t2 = time.time() - time_t1
time_p2 = time.process_time() - time_p1
print(count)
print('実時間 ', time_t2)
print('CPU時間', time_p2)
結果
code:result1.txt
1000000000
実時間 34.881747245788574
CPU時間 34.8817224
予想に反して、実時間とCPU時間に大差はみられなかった。以降はCPU時間を用いて評価する。
(補足:繰り返して実験すると、結果は約30-35秒の範囲で変動することに注意)
計算過程を可視化するために、tqdmモジュールを適用する。
code:jikan2.py
import time
import tqdm
loopmax = 1000000000
count = 0
time1 = time.process_time()
#
for i in tqdm.tqdm(range(loopmax)):
count += 1
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
結果
code:result2.txt
1000000000
CPU時間 80.7644173
計算過程はわかりやすくなってよろしいのだが、要した時間は34から80へと2.4倍程度に増加していることに留意しよう。
毎回のループ処理において、if文で判定した結果をもとに進捗を表示するようにした。
code:jikan3.py
import time
loopmax = 1000000000
count = 0
d = loopmax // 10
time1 = time.process_time()
#
for i in range(loopmax):
count += 1
if i % d == 0:
print(int((i / loopmax)*100 + 10), '% progress')
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
結果
code:result3.txt
10 % progress
20 % progress
30 % progress
40 % progress
50 % progress
60 % progress
70 % progress
80 % progress
90 % progress
100 % progress
1000000000
CPU時間 53.000970900000006
ここまでをまとめると
table:計算時間の比較
手法 実行時間 増加率
なし 34.88
tqdm 80.76 2.5倍
if文 53.00 1.52倍
スピード重視であれば、過程は出力しない
tqdmは簡単に計算過程を出力できるが負荷が高い
今回、1000000000=10億回のループ処理を行っているので、ループ回数がより少ない場合には、tqdmやif文による判定の負荷は減ると予想する
検証のために上の処理を2重ループにしてみた。進捗の判定は外側のループでloopmax_i回だけ行われるので、1000/100000000 = 1e-0.5倍に低減化されたことになる。
code:jikan4.py
import time
loopmax_i = 1000
loopmax_j = 1000000
count = 0
time1 = time.process_time()
#
for i in range(loopmax_i):
for j in range(loopmax_j):
count += 1
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
結果
code:result4.txt
1000000000
CPU時間 29.803346
code:jikan5.py
import time
import tqdm
loopmax_i = 1000
loopmax_j = 1000000
count = 0
time1 = time.process_time()
#
for i in tqdm.tqdm(range(loopmax_i)):
for j in range(loopmax_j):
count += 1
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
code:result5.txt
100%|███████████████████████████████████████████████████████████████████████████████| 1000/1000 00:30<00:00, 32.44it/s 1000000000
CPU時間 30.8358749
予想通り、計算時間は大幅に短縮された。
~~~~~~~~~~~~~~~~~~~~~~~~
最後に、関数を用いることによるオーバーヘッドを評価する。
code:jikan6.py
import time
loopmax = 1000000000
count = 0
def countpp(c):
c += 1
time1 = time.process_time()
#
for i in range(loopmax):
countpp(count)
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
結果
code:kresult6.txt
1000000000
CPU時間 54.0426542
関数を用いない場合は約30秒程度であったことから、54/30 = 1.8倍程度に増加している。こんな単純な関数であっても、呼び出す回数が多い場合(この場合1億回)には大きな影響があることが確認できた。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
興味本位で、tqdm + 関数の場合を作ってみた。
code:jikan7.py
import time
import tqdm
loopmax = 1000000000
count = 0
def countpp(c):
c += 1
return c
time1 = time.process_time()
#
for i in tqdm.tqdm(range(loopmax)):
count = countpp(count)
#
time2 = time.process_time() - time1
print(count)
print('CPU時間', time2)
結果
code:result7.txt
1000000000
CPU時間 106.71095009999999
106/30 = 3.5倍!
まとめ
プログラムの実行時間はCPU時間で図る、しかしながら、試行の度に結果は変動するため、重要な局面では何度も実行し、統計的な値で評価する必要がある。(平均や分散など)
進捗状況を把握することは、数値実験をおこなう上で大切なポイントではあるが、パフォーマンスの低下をもたらす。開発中は表示し、本番では無効化するような仕組みがあるとよい。
プログラムを読みやすく・作りやすくするために関数は効果的であるが、関数を使わないことで高速化することができる。その分プログラムが冗長になる。